﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows;

namespace MTS
{
    [Serializable()]
    class LevelChunk : IComparable<LevelChunk>, IEquatable<LevelChunk>
    {
        int mapX = 16;
        int mapY = 16;
        int mapZ = 128;
        FileInfo _fileInfo;
        private long _worldX;
        private long _worldY;
        private Boolean _hasBlocks = false;
        private byte[] _terrainImage;
        private byte[] _obliqueImage;
        private int bytesPerPixel;
        DateTime lastTerrainWritten;
        DateTime lastObliqueWritten;

        public byte[] terrainImage
        {
            get
            {
                if (((_terrainImage == null) && (this.lastTerrainWritten == null)) || (this.lastTerrainWritten < _fileInfo.LastWriteTime))
                {
                    this.lastTerrainWritten = _fileInfo.LastWriteTime;
                    this._terrainImage = renderTerrain();
                }
                return _terrainImage;
            }
        }

        public byte[] obliqueImage
        {
            get
            {
                if (((_obliqueImage == null) && (this.lastObliqueWritten == null)) || (this.lastObliqueWritten < _fileInfo.LastWriteTime))
                {
                    this.lastObliqueWritten = _fileInfo.LastWriteTime;
                    this._obliqueImage = renderOblique();
                }
                return _obliqueImage;
            }
        }

        public FileInfo fileInfo
        {
            get
            {
                return this._fileInfo;
            }
        }

        public Int32Rect terrainRect
        {
            get
            {
                return new Int32Rect((int)worldX, (int)worldY, 16, 16);
            }
        }

        public Int32Rect obliqueRect
        {
            get
            {
                return new Int32Rect((int)worldX, (int)worldY, 16, 145);
            }
        }

        public Boolean hasBlocks
        {
            get
            {
                return _hasBlocks;
            }
        }
        public long worldY
        {
            get
            {
                return _worldY;
            }
        }
        public long worldX
        {
            get
            {
                return _worldX;
            }
        }

        public LevelChunk(FileInfo fileInfo, int bytesPerPixel)
        {
            this._fileInfo = fileInfo;
            this.bytesPerPixel = bytesPerPixel;
            char[] delimiters = {'.'};
            String[] parts = fileInfo.Name.Split(delimiters);
            if (parts.Length == 4)
            {
                this._worldX = this.DecodeBase36(parts[2].Trim())*-1;
                this._worldY = this.DecodeBase36(parts[1].Trim());

                this._hasBlocks = true;
            }
            
        }

        private byte[] renderTerrain()
        {
            BlockColors colorer = new BlockColors();
            int _stride = this.mapX * bytesPerPixel;
            byte[] outputBytes = new byte[this.mapX * this.mapY * bytesPerPixel];
            byte[] blocks = this.loadBlocks();
            for (int z = 0; z < this.mapZ; z++)
            {
                for (int y = 0; y < this.mapY; y++)
                {
                    for (int x = 0; x < this.mapX; x++)
                    {
                        int offset = x * mapZ + y * mapZ * mapX + z;
                        int pixelOffset = ((this.mapX - x - 1) + y * mapX) * bytesPerPixel;
                        byte tile = blocks[offset];
                        Color existingColor = Color.FromArgb(outputBytes[pixelOffset + 3], outputBytes[pixelOffset + 2], outputBytes[pixelOffset + 1], outputBytes[pixelOffset]);
                        Color tileColor = colorer.getBlockColor(tile);
                        // Drawing code here
                        if (tileColor.A == 255)
                        {
                            // No point alpha blending if the new tile is fully solid
                            outputBytes[pixelOffset] = tileColor.B;
                            outputBytes[pixelOffset + 1] = tileColor.G;
                            outputBytes[pixelOffset + 2] = tileColor.R;
                            outputBytes[pixelOffset + 3] = tileColor.A;
                        }
                        else if (tileColor.A > 0)
                        {
                            Color newColor = blendColors(tileColor, existingColor, z);
                            outputBytes[pixelOffset] = newColor.B;
                            outputBytes[pixelOffset + 1] = newColor.G;
                            outputBytes[pixelOffset + 2] = newColor.R;
                            outputBytes[pixelOffset + 3] = newColor.A;
                        }
                    }
                }
            }
            
            return outputBytes;
        }

        private byte[] renderOblique()
        {
            BlockColors colorer = new BlockColors();
            int _stride = this.mapX * bytesPerPixel;
            byte[] outputBytes = new byte[this.mapX * (this.mapY + this.mapZ + 1)* bytesPerPixel];
            byte[] blocks = this.loadBlocks();
            for (int z = 0; z < this.mapZ; z++)
            {
                for (int y = 0; y < this.mapY; y++)
                {
                    for (int x = 0; x < this.mapX; x++)
                    {
                        int offset = x * mapZ + y * mapZ * mapX + z;
                        int pixelOffset = ((this.mapX - x - 1) + ((this.mapZ - z - 1) + y) * mapX) * bytesPerPixel;
                        int pixelOffsetAbove = ((this.mapX - x - 1) + ((this.mapZ - z - 1) + y + 1) * mapX) * bytesPerPixel;
                        byte tile = blocks[offset];
                        Color existingColor = Color.FromArgb(outputBytes[pixelOffset + 3], outputBytes[pixelOffset + 2], outputBytes[pixelOffset + 1], outputBytes[pixelOffset]);
                        Color tileColor = colorer.getBlockColor(tile);

                        if (tileColor.A.CompareTo(0) > 0)
                        {
                            outputBytes[pixelOffset] = tileColor.B;
                            outputBytes[pixelOffset + 1] = tileColor.G;
                            outputBytes[pixelOffset + 2] = tileColor.R;
                            outputBytes[pixelOffset + 3] = 0xff;
                            outputBytes[pixelOffsetAbove] = (byte)Math.Max((int)tileColor.B - 20, 0);
                            outputBytes[pixelOffsetAbove + 1] = (byte)Math.Max((int)tileColor.G - 20, 0);
                            outputBytes[pixelOffsetAbove + 2] = (byte)Math.Max((int)tileColor.R - 20, 0);
                            outputBytes[pixelOffsetAbove + 3] = 0xff;
                        }
                    }
                }
            }
            return outputBytes;
        }

        private byte[] loadBlocks()
        {
            if (this.hasBlocks)
            {
                byte[] blockTagTitle = new byte[9];
                blockTagTitle[0] = 0x07;
                blockTagTitle[1] = 0x00;
                blockTagTitle[2] = 0x06;
                blockTagTitle[3] = 0x42;
                blockTagTitle[4] = 0x6c;
                blockTagTitle[5] = 0x6f;
                blockTagTitle[6] = 0x63;
                blockTagTitle[7] = 0x6b;
                blockTagTitle[8] = 0x73;

                byte[] heightTagTitle = new byte[12];
                heightTagTitle[0] = 0x07;
                heightTagTitle[1] = 0x00;
                heightTagTitle[2] = 0x09;
                heightTagTitle[3] = 0x48;
                heightTagTitle[4] = 0x65;
                heightTagTitle[5] = 0x69;
                heightTagTitle[6] = 0x67;
                heightTagTitle[7] = 0x68;
                heightTagTitle[8] = 0x74;
                heightTagTitle[9] = 0x4d;
                heightTagTitle[10] = 0x61;
                heightTagTitle[11] = 0x70;

                byte[] rawMap = this.loadLevelChunk(_fileInfo);

                using (MemoryStream bytesIn = new MemoryStream(rawMap))
                {
                    BinaryReader inReader = new BinaryReader(bytesIn);
                    int position = -1;
                    for (int index = 0; index < rawMap.Length - blockTagTitle.Length; index++)
                    {
                        if (rawMap[index] == blockTagTitle[0]
                            && rawMap[index + 1] == blockTagTitle[1]
                            && rawMap[index + 2] == blockTagTitle[2]
                            && rawMap[index + 3] == blockTagTitle[3]
                            && rawMap[index + 4] == blockTagTitle[4]
                            && rawMap[index + 5] == blockTagTitle[5]
                            && rawMap[index + 6] == blockTagTitle[6]
                            && rawMap[index + 7] == blockTagTitle[7]
                            && rawMap[index + 8] == blockTagTitle[8]
                            )
                        {
                            position = index;
                            break;
                        }

                    }
                    if (position != -1 && position < rawMap.Length)
                    {
                        inReader.BaseStream.Seek(position + 9, SeekOrigin.Begin);
                        byte[] lengthBytes = inReader.ReadBytes(4);
                        if (BitConverter.IsLittleEndian)
                        {
                            Array.Reverse(lengthBytes);
                        }
                        int length = BitConverter.ToInt32(lengthBytes, 0);
                        byte[] blocks = inReader.ReadBytes(length);
                        return blocks;
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            else
            {
                return null;
            }
        }

        private byte[] loadLevelChunk(FileInfo fileInfo)
        {
            using (FileStream inFile = fileInfo.OpenRead())
            {
                using (MemoryStream bytesOut = new MemoryStream())
                {
                    using (GZipStream decompressor = new GZipStream(inFile, CompressionMode.Decompress))
                    {
                        decompressor.CopyTo(bytesOut);
                    }
                    return bytesOut.ToArray();
                }
            }
        }

        public String getFileName()
        {
            return this._fileInfo.Name;
        }

        private long DecodeBase36(string Base36String)
        {
            string chars = "0123456789abcdefghijklmnopqrstuvwxyz";

            bool Negative = Base36String.StartsWith("-");
            if (Negative)
                Base36String = Base36String.TrimStart(new char[] { '-' });

            Stack<char> InputStack = new Stack<char>();
            foreach (char c in Base36String.ToLower())
            {
                InputStack.Push(c);
            }
            StringBuilder InputString = new StringBuilder();
            while (InputStack.Count > 0)
            {
                InputString.Append(InputStack.Pop());
            }

            long Output = 0; int Position = 0;
            foreach (char c in InputString.ToString())
            {
                Output += chars.IndexOf(c) * (long)Math.Pow(36, Position);
                Position++;
            }

            return (Negative ? Output * -1 : Output);

        }

        private Color blendColors(Color A, Color B, int h)
        {
            Color C = new Color();
            float Aa = A.ScA;
            float Ba = B.ScA;
            float Alpha = Aa + Ba * (1 - Aa);
            C.R = (byte)((float)A.R * Aa + (float)B.R * Ba * (1 - Aa) * (float)h/128 / Alpha);
            C.G = (byte)(float)(A.G * Aa + (float)B.G * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.B = (byte)((float)A.B * Aa + (float)B.B * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.A = (byte)(Alpha * 255);
            return C;
        }

        public int CompareTo(LevelChunk other)
        {
            int yComp = worldY.CompareTo(other.worldY);
            if (yComp != 0)
            {
                return yComp;
            }
            else
            {
                return worldX.CompareTo(other.worldX);
            }
        }

        public bool Equals(LevelChunk other)
        {
            return _fileInfo.FullName.Equals(other._fileInfo.FullName);
        }

    }
}
